Skip to content

Fix/160 리팩토링#161

Merged
evaain706 merged 6 commits intodevelopfrom
fix/160
Feb 21, 2026
Merged

Fix/160 리팩토링#161
evaain706 merged 6 commits intodevelopfrom
fix/160

Conversation

@evaain706
Copy link
Contributor

@evaain706 evaain706 commented Feb 21, 2026

📌 변경 사항 개요

  • 리팩토링

📝 상세 내용

  • 예약 인터페이스 과거월로 이동못하게 수정
  • 체험 상세페이지 SSR리팩토링
  • 등록/수정폼 이미지 api호출 플로우 개선

🔗 관련 이슈

🖼️ 스크린샷(선택사항)

💡 참고 사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 예약(Booking) 섹션 추가로 예약 흐름이 분리 및 개선됨
    • 로딩 스켈레톤 추가로 대기 중 UI 경험 향상
  • 개선사항

    • 이미지 반응형 사이즈 최적화로 이미지 로딩 성능 개선
    • 제목 영역 소유자 판별 방식 개선으로 편집/삭제 흐름 안정화
    • 날짜 선택기에서 이전 월 네비게이션 비활성화로 과거 선택 방지
    • 생성/수정/삭제/리뷰 동작 시 관련 활동 캐시 자동 재검증 적용
  • 수정

    • 상세 페이지 렌더링 및 데이터 로드 흐름 개선으로 오류 처리 강화

@evaain706 evaain706 self-assigned this Feb 21, 2026
@evaain706 evaain706 added the 🔨 Refactor 코드 리팩토링 label Feb 21, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

Warning

Rate limit exceeded

@evaain706 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 40 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

ActivityDetailForm 클라이언트를 제거하고, 서버 측 비동기 페이지에서 활동 데이터를 직접 페치하도록 재구성했으며 예약 로직을 BookingSection으로 분리하고 이미지 최적화와 캐시 무효화 지점을 추가했습니다.

Changes

Cohort / File(s) Summary
페이지 데이터 페칭 및 구성 변경
src/app/(with-header)/activities/[id]/page.tsx, src/app/(with-header)/activities/[id]/components/ActivityDetailForm.tsx
클라이언트 기반 ActivityDetailForm 삭제. page.tsx를 async 서버 컴포넌트로 변경해 API에서 활동 데이터를 페치하고 404 처리를 수행하며 전체 레이아웃을 구성하도록 이동.
예약 로직 분리
src/app/(with-header)/activities/[id]/components/BookingSection.tsx
BookingSection 신규 추가: 소유자 체크, 연/월 상태, 현재/이전/다음 달 스케줄 병렬 조회 및 병합 후 BookingInterface에 전달.
로딩 스켈레톤 추가
src/app/(with-header)/activities/[id]/loading.tsx
활동 상세 페이지용 로딩 스켈레톤 컴포넌트 추가(이미지, 설명, 예약, 리뷰 자리표시).
타이틀 소유권 처리 변경
src/app/(with-header)/activities/[id]/components/Title.tsx, src/types/activityDetailType.ts
Title 컴포넌트 프로퍼티를 isOwner → userId로 변경하고 내부에서 현재 사용자와 비교해 소유권 판단하도록 수정. 관련 타입 업데이트.
이미지 최적화
src/app/(with-header)/activities/[id]/components/ImageGrid.tsx
Next.js Image에 responsive sizes, priority, modal 이미지 quality 추가 및 unoptimized 제거로 로딩/품질 제어 개선.
달력 내비게이션 제약
src/components/DatePicker/DatePicker.tsx, src/components/DatePicker/CalendarHeader.tsx, src/types/datePickerTypes.ts
이전 달 이동 비활성화 플래그 isPrevDisabled 추가 및 관련 UI/로직 반영으로 과거 월 네비게이션 방지.
캐시 무효화 통합
src/app/api/deleteActivity/[id]/route.ts, src/app/api/editActivity/[id]/route.ts, src/app/api/reservations/[id]/reviews/route.ts, src/app/(with-header)/myactivity/[id]/hooks/useEditActivityForm.ts
활동 삭제/수정 및 리뷰 생성 후 revalidateTag 호출로 해당 활동 캐시 태그 무효화 추가.
이미지 업로드 흐름 변경
src/app/(with-header)/myactivity/hooks/useCreateActivityForm.ts
메인/서브 이미지 핸들러를 비동기 → 동기로 변경하고, 업로드 및 URL 생성 로직을 폼 제출 단계로 이동시켜 훅의 공개 API 단순화.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser as Client (page)
    participant Store as User Store
    participant API as Public Activity API
    participant PrivateAPI as Private Schedule API

    User->>Browser: 활동 상세 페이지 요청 (id)
    Browser->>API: 활동 데이터 fetch(id)
    API-->>Browser: 활동 데이터 (또는 404)

    alt 404
        Browser->>Browser: notFound()
    else 성공
        Browser->>Store: 현재 사용자 ID 조회
        Store-->>Browser: currentUserId
        Browser->>Browser: 소유권 계산 (userId === currentUserId)
        alt 소유자 아님
            Browser->>PrivateAPI: 월별 스케줄 요청 (현재/이전/다음)
            PrivateAPI-->>Browser: 스케줄 응답들
            Browser->>Browser: 스케줄 병합 및 BookingSection 렌더
        else 소유자
            Browser->>Browser: BookingSection 미렌더링
        end
        Browser->>Browser: ImageGrid, Title, LocationMap, ReviewSection 렌더링
        Browser-->>User: 페이지 표시
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • LeeCh0129
  • BokyungCodes
  • minimo-9

Poem

🐰 페이지를 살짝 정리했어요,
폼은 떠나고 섹션은 새로 왔지요,
이미지 빛나고 스케줄 합쳐지니,
캐시는 톡톡 무효화되고,
토끼는 당근 들고 축하 춤 춥니다 🥕🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목 'Fix/160'은 지라 이슈 번호를 참조하지만 실제 변경사항을 설명하지 않습니다. 변경사항은 SSR 리팩토링, 과거 월 네비게이션 방지, 이미지 업로드 흐름 개선 등 여러 주요 사항을 포함하고 있습니다. PR 제목을 'Refactor activity detail page for SSR and improve booking/image workflows'와 같이 주요 변경사항을 명확히 요약하도록 수정하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/160

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/DatePicker/DatePicker.tsx (1)

55-65: ⚠️ Potential issue | 🟠 Major

디버그용 console.log 제거 필요

프로덕션 코드에 디버그 로그가 남아 있습니다.

  • Lines 55–65: useEffect 전체 본문이 console.log 호출만으로 구성되어 실질적인 사이드 이펙트가 없으며, highlightDates도 의존성 배열에서 누락되어 있습니다 (react-hooks/exhaustive-deps 위반). 이 useEffect 자체를 제거해야 합니다.
  • Line 81: 컴포넌트 바디 최상위 레벨에 위치한 console.log로, 매 렌더마다 실행되어 성능에 영향을 주고 내부 상태를 노출합니다.
🔧 제안하는 수정
- useEffect(() => {
-   console.log(
-     '가능날짜',
-     availableDates.map((d) => d.date),
-   );
-   console.log(
-     '하이라이트날짜',
-     highlightDates.map((d) => d.format('YYYY-MM-DD')),
-   );
-   console.log('뷰데이트', viewDate.format('YYYY-MM-DD'));
- }, [availableDates, viewDate]);

  const isPrevDisabled = ...

  const changeMonth = ...

- console.log('schedules', schedules);

  const handleDateSelect = ...

Also applies to: 81-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DatePicker/DatePicker.tsx` around lines 55 - 65, Remove the
debug console.logs: delete the entire useEffect that only logs availableDates,
highlightDates, and viewDate (the useEffect referencing availableDates and
viewDate) and remove the top-level console.log in the component body; ensure you
do not rely on that effect for any side effects and that any real effects
include all needed deps (e.g., add highlightDates to deps if you later
reintroduce an effect) — search for the useEffect containing availableDates.map
and highlightDates.map and the standalone console.log at the component's top
level to locate and remove them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(with-header)/activities/[id]/components/BookingSection.tsx:
- Around line 61-66: Remove the unnecessary setTimeout wrapper in
handleMonthChange inside BookingSection.tsx: call setYear(year) and
setMonth(month) directly (no delayed callback) since React 18+ batches state
updates; keep the useCallback for handleMonthChange but you can leave the
dependency array empty (setState setters are stable) or include setYear/setMonth
if you prefer explicit deps.
- Around line 50-54: The filter uses PromiseFulfilledResult<any> which triggers
no-explicit-any; replace the any with the actual fulfilled value type by
declaring or reusing the correct type for the responses (e.g., SideItem or the
API response type) and update the type guard on sideResults to (r): r is
PromiseFulfilledResult<TheRealType> => r.status === 'fulfilled' so flatMap((r)
=> r.value.data) has a concrete type; locate the sideResults variable and the
data shape used in BookingSection.tsx to pick the exact type (or import an
existing interface) and use that type in the PromiseFulfilledResult generic.

In `@src/app/`(with-header)/activities/[id]/components/Title.tsx:
- Around line 27-30: Extract the duplicated owner-check logic (currently using
useUserStore and computing currentUserId and isOwner in Title.tsx,
BookingSection.tsx, and ActivityDetailSkeleton.tsx) into a small reusable hook
named useIsOwner(userId) that returns a boolean; implement useIsOwner to read
currentUserId from useUserStore (same selector used today) and return
currentUserId != null && currentUserId === userId, then replace the inline logic
in Title.tsx, BookingSection.tsx, and ActivityDetailSkeleton.tsx to call
useIsOwner(userId) instead of duplicating the selector and comparison.

In `@src/app/`(with-header)/activities/[id]/loading.tsx:
- Around line 4-72: Extract the duplicated skeleton markup into a single shared
presentational component (e.g., ActivityDetailSkeleton or
SharedActivitySkeleton) and have both the server Loading() and the existing
ActivityDetailSkeleton.tsx consume it; move purely UI pieces (the grid,
placeholders, and repeated ReviewCardSkeleton usage) into the new shared
component and keep client-only bits like SkeletonBookingInterface or
ReviewCardSkeleton usage either inside a small client wrapper component or
passed as slots/props so Loading (a server component) can render the shared
layout without importing client-only modules directly; update Loading and
src/app/.../ActivityDetailSkeleton.tsx to import and render the new shared
component, preserving keys like ReviewCardSkeleton and SkeletonBookingInterface
by wrapping them in a client component if needed.
- Line 6: The outer container div that currently has the class 'animate-pulse'
is causing nested animation with the child skeleton components
(ReviewCardSkeleton and SkeletonBookingInterface); remove the 'animate-pulse'
class from the outer div (the div with className 'mx-auto max-w-1200 ...') so
that only the inner components control their own pulse animation, ensuring no
overlapping/duplicated animation occurs and keeping ReviewCardSkeleton and
SkeletonBookingInterface as the sole owners of their skeleton animation.

In `@src/app/`(with-header)/activities/[id]/page.tsx:
- Around line 54-78: Create a small client-side wrapper component (e.g.,
ActivityDetailClientLayout or BookingSectionWrapper) that determines current
user ownership and renders the outer grid with md:grid-cols-2 when the viewer is
the owner and md:grid-cols-3 otherwise; move the markup currently in page.tsx
(the div with mt-86 grid ... and its children including BookingSection,
LocationMap, ReviewSection) into that wrapper and use BookingSection (which may
return null) inside it so the wrapper can adjust the grid class based on
isOwner. Reference BookingSection and ActivityDetailSkeleton behavior as a model
for switching grid-cols depending on ownership; keep BookingSection
implementation unchanged and ensure page.tsx imports and uses the new client
wrapper instead of the static md:grid-cols-3 div.
- Around line 30-33: Change the catch parameter from any to unknown and add a
type guard (e.g., isNotFoundError(error: unknown): error is Error & { digest:
string }) that checks error instanceof Error and that error.digest ===
'NEXT_NOT_FOUND'; in the catch block use that guard to rethrow the
NEXT_NOT_FOUND error and otherwise throw the existing new Error('활동 상세 데이터 조회
실패') so you avoid using the any type while preserving the current behavior.

In `@src/app/`(with-header)/myactivity/hooks/useCreateActivityForm.ts:
- Around line 39-67: The current flow uploads bannerImageUrl and subImageUrls
via uploadImage before calling privateInstance.post('/addActivity'), which can
leave orphaned uploaded files if the POST fails; wrap the upload + POST sequence
in a try/catch and on any failure delete any already-uploaded images: track
uploaded URLs in bannerImageUrl and subImageUrls, and in the catch call your
storage deletion helper (e.g., deleteImage or an API endpoint you add) for each
uploaded URL, then rethrow or surface the error; update useCreateActivityForm to
perform uploads, attempt privateInstance.post('/addActivity', payload) inside
the same try, and cleanup uploaded images on catch to avoid orphaned resources.
- Around line 47-54: Replace the sequential for...of+await loop that uploads
each entry in subImage with a parallel Promise.all approach: map subImage to an
array of promises where each element returns uploadImage(img) if img is a File
or returns the img string otherwise, then await Promise.all on that array and
assign the resulting array to subImageUrls (or push the results) so uploads
happen concurrently; locate the logic in useCreateActivityForm where subImage,
uploadImage, and subImageUrls are referenced.

In `@src/app/api/editActivity/`[id]/route.ts:
- Line 40: The call to revalidateTag in the Route Handler uses the deprecated
single-argument form; update the invocation of revalidateTag(`activity-${id}`)
to the recommended two-argument form revalidateTag(`activity-${id}`, 'max') so
TypeScript errors are resolved and the tag is revalidated using SWR semantics;
locate the usage of revalidateTag in route.ts and change the call accordingly.

In `@src/app/api/reservations/`[id]/reviews/route.ts:
- Around line 42-44: Remove the unnecessary conditional guard and always call
revalidateTag using the required activityId from the ReviewResponse;
specifically, in the route handler replace the if-check around revalidateTag and
directly invoke revalidateTag(`activity-${response.data.activityId}`), relying
on the ReviewResponse.type (activityId: number) and the existing single-argument
revalidateTag usage.

---

Outside diff comments:
In `@src/components/DatePicker/DatePicker.tsx`:
- Around line 55-65: Remove the debug console.logs: delete the entire useEffect
that only logs availableDates, highlightDates, and viewDate (the useEffect
referencing availableDates and viewDate) and remove the top-level console.log in
the component body; ensure you do not rely on that effect for any side effects
and that any real effects include all needed deps (e.g., add highlightDates to
deps if you later reintroduce an effect) — search for the useEffect containing
availableDates.map and highlightDates.map and the standalone console.log at the
component's top level to locate and remove them.

Comment on lines +61 to +66
const handleMonthChange = useCallback((year: number, month: number) => {
setTimeout(() => {
setYear(year);
setMonth(month);
});
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

setTimeout 래핑이 불필요할 수 있습니다.

React 18+에서는 상태 업데이트가 자동으로 배치 처리되므로, setTimeout으로 감싸지 않아도 setYearsetMonth가 하나의 리렌더링으로 처리됩니다. 특별한 이유가 없다면 제거를 고려해 보세요.

♻️ 수정 제안
  const handleMonthChange = useCallback((year: number, month: number) => {
-    setTimeout(() => {
-      setYear(year);
-      setMonth(month);
-    });
+    setYear(year);
+    setMonth(month);
   }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleMonthChange = useCallback((year: number, month: number) => {
setTimeout(() => {
setYear(year);
setMonth(month);
});
}, []);
const handleMonthChange = useCallback((year: number, month: number) => {
setYear(year);
setMonth(month);
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/activities/[id]/components/BookingSection.tsx around
lines 61 - 66, Remove the unnecessary setTimeout wrapper in handleMonthChange
inside BookingSection.tsx: call setYear(year) and setMonth(month) directly (no
delayed callback) since React 18+ batches state updates; keep the useCallback
for handleMonthChange but you can leave the dependency array empty (setState
setters are stable) or include setYear/setMonth if you prefer explicit deps.

Comment on lines +27 to +30
const currentUserId = useUserStore((state) =>
state.user ? state.user.id : null,
);
const isOwner = currentUserId != null && userId != null && currentUserId === userId;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

소유자 판별 로직이 여러 컴포넌트에 중복되어 있습니다.

Title.tsx, BookingSection.tsx, ActivityDetailSkeleton.tsx에서 동일한 useUserStore + isOwner 계산 패턴이 반복됩니다. 간단한 커스텀 훅으로 추출하면 일관성과 유지보수성을 높일 수 있습니다.

♻️ 리팩토링 제안
// hooks/useIsOwner.ts
import useUserStore from '@/stores/authStore';

export function useIsOwner(userId: number): boolean {
  const currentUserId = useUserStore((state) =>
    state.user ? state.user.id : null,
  );
  return currentUserId != null && currentUserId === userId;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/activities/[id]/components/Title.tsx around lines 27 -
30, Extract the duplicated owner-check logic (currently using useUserStore and
computing currentUserId and isOwner in Title.tsx, BookingSection.tsx, and
ActivityDetailSkeleton.tsx) into a small reusable hook named useIsOwner(userId)
that returns a boolean; implement useIsOwner to read currentUserId from
useUserStore (same selector used today) and return currentUserId != null &&
currentUserId === userId, then replace the inline logic in Title.tsx,
BookingSection.tsx, and ActivityDetailSkeleton.tsx to call useIsOwner(userId)
instead of duplicating the selector and comparison.

Comment on lines +4 to +72
export default function Loading() {
return (
<div className='mx-auto max-w-1200 animate-pulse p-4 sm:px-20 lg:p-8'>
{/* 타이틀 */}
<div className='mb-6 flex items-start justify-between'>
<div className='flex w-full flex-col gap-10'>
<div className='h-16 w-24 rounded bg-gray-300' />
<div className='h-42 w-3/4 rounded bg-gray-300' />
<div className='flex gap-10'>
<div className='h-20 w-50 rounded bg-gray-300' />
<div className='h-20 w-170 rounded bg-gray-300' />
</div>
</div>
</div>

{/* 이미지그리드 */}
<div className='relative block aspect-square h-[300px] w-full overflow-hidden rounded-lg bg-gray-300 md:hidden' />
<div className='hidden h-[500px] grid-cols-4 grid-rows-4 gap-6 md:grid'>
<div className='col-span-2 row-span-4 rounded-lg bg-gray-300' />
{[...Array(4)].map((_, i) => (
<div
key={i}
className='col-span-1 row-span-2 rounded-lg bg-gray-300'
/>
))}
</div>

{/* 설명/예약인터페이스/장소 */}
<div className='mt-86 grid gap-10 grid-cols-1 md:grid-cols-3'>
{/* 설명 */}
<div className='md:col-span-2'>
<div className='mb-10 h-34 w-90 rounded bg-gray-300' />
<div className='mb-4 h-180 w-full rounded bg-gray-300' />
</div>

{/* 예약인터페이스 */}
<div className='md:row-span-2'>
<SkeletonBookingInterface />
</div>

{/* 체험 장소/리뷰 */}
<div className='md:col-span-2 space-y-8'>
{/* 장소 */}
<div className='mb-40'>
<div className='mb-10 h-34 w-90 rounded bg-gray-300' />
<div className='h-[480px] w-full rounded-lg bg-gray-400 shadow-md' />
<div className='mt-8 flex items-center space-x-3'>
<div className='h-6 w-6 rounded-full bg-gray-300' />
<div className='h-20 w-1/2 rounded bg-gray-300' />
</div>
</div>

{/* 리뷰 */}
<div>
<div className='mt-10 flex flex-col space-y-8'>
<div className='mb-10 h-34 w-50 rounded bg-gray-300' />
<div className='mb-5 h-50 w-120 rounded bg-gray-300' />
<div className='relative min-h-450 flex-col gap-30'>
{[...Array(3)].map((_, index) => (
<ReviewCardSkeleton key={index} />
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

ActivityDetailSkeleton과의 코드 중복을 고려해 보세요.

src/app/(with-header)/activities/[id]/components/Skeletons/ActivityDetailSkeleton.tsx에 거의 동일한 스켈레톤 마크업이 존재합니다. loading.tsx는 서버 컴포넌트라 소유자 판별이 불가하지만, 공통 레이아웃 부분을 공유 컴포넌트로 추출하면 유지보수성을 높일 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/activities/[id]/loading.tsx around lines 4 - 72,
Extract the duplicated skeleton markup into a single shared presentational
component (e.g., ActivityDetailSkeleton or SharedActivitySkeleton) and have both
the server Loading() and the existing ActivityDetailSkeleton.tsx consume it;
move purely UI pieces (the grid, placeholders, and repeated ReviewCardSkeleton
usage) into the new shared component and keep client-only bits like
SkeletonBookingInterface or ReviewCardSkeleton usage either inside a small
client wrapper component or passed as slots/props so Loading (a server
component) can render the shared layout without importing client-only modules
directly; update Loading and src/app/.../ActivityDetailSkeleton.tsx to import
and render the new shared component, preserving keys like ReviewCardSkeleton and
SkeletonBookingInterface by wrapping them in a client component if needed.


export default function Loading() {
return (
<div className='mx-auto max-w-1200 animate-pulse p-4 sm:px-20 lg:p-8'>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

외부 컨테이너의 animate-pulse가 내부 자식 요소의 animate-pulse와 중첩됩니다.

Line 6의 외부 divanimate-pulse가 적용되어 있지만, ReviewCardSkeletonSkeletonBookingInterface 내부 요소들에도 각각 animate-pulse가 개별적으로 적용되어 있습니다. 이로 인해 애니메이션이 중첩되어 의도치 않은 시각적 깜빡임이 발생할 수 있습니다.

외부 animate-pulse를 제거하거나, 내부 컴포넌트의 개별 animate-pulse를 제거하여 한 레벨에서만 적용하는 것을 권장합니다.

🛠️ 수정 제안
-    <div className='mx-auto max-w-1200 animate-pulse p-4 sm:px-20 lg:p-8'>
+    <div className='mx-auto max-w-1200 p-4 sm:px-20 lg:p-8'>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className='mx-auto max-w-1200 animate-pulse p-4 sm:px-20 lg:p-8'>
<div className='mx-auto max-w-1200 p-4 sm:px-20 lg:p-8'>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/activities/[id]/loading.tsx at line 6, The outer
container div that currently has the class 'animate-pulse' is causing nested
animation with the child skeleton components (ReviewCardSkeleton and
SkeletonBookingInterface); remove the 'animate-pulse' class from the outer div
(the div with className 'mx-auto max-w-1200 ...') so that only the inner
components control their own pulse animation, ensuring no overlapping/duplicated
animation occurs and keeping ReviewCardSkeleton and SkeletonBookingInterface as
the sole owners of their skeleton animation.

Comment on lines +54 to +78
<div className='mt-86 grid gap-15 grid-cols-1 md:grid-cols-3'>
<div className='md:col-span-2'>
<h2 className='mb-4 pb-2 text-2xl font-bold'>체험 설명</h2>
<p className='leading-relaxed whitespace-pre-line'>
{activityData.description}
</p>
</div>

<BookingSection
activityId={id}
userId={activityData.userId}
price={activityData.price}
/>

<div className='md:col-span-2'>
<h2 className='mb-4 pb-2 text-2xl font-bold'>체험 장소</h2>
<LocationMap address={activityData.address ?? ''} />

<ReviewSection
activityId={Number(id)}
reviewCount={activityData.reviewCount}
rating={activityData.rating}
/>
</div>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

소유자(Owner)일 때 레이아웃이 3열 그리드에서 비어 보일 수 있습니다.

BookingSection이 소유자에게 null을 반환하지만, 그리드는 항상 md:grid-cols-3으로 설정되어 있습니다. 소유자의 경우 예약 인터페이스가 없으므로 불필요한 여백이 생길 수 있습니다. ActivityDetailSkeleton에서는 소유자 여부에 따라 grid-cols-2/grid-cols-3을 동적으로 전환하고 있습니다.

서버 컴포넌트에서는 클라이언트 상태(현재 사용자)에 접근할 수 없으므로, 이 레이아웃 조정을 BookingSection 내부의 wrapper로 처리하거나, 별도의 클라이언트 레이아웃 컴포넌트로 분리하는 것을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/activities/[id]/page.tsx around lines 54 - 78, Create
a small client-side wrapper component (e.g., ActivityDetailClientLayout or
BookingSectionWrapper) that determines current user ownership and renders the
outer grid with md:grid-cols-2 when the viewer is the owner and md:grid-cols-3
otherwise; move the markup currently in page.tsx (the div with mt-86 grid ...
and its children including BookingSection, LocationMap, ReviewSection) into that
wrapper and use BookingSection (which may return null) inside it so the wrapper
can adjust the grid class based on isOwner. Reference BookingSection and
ActivityDetailSkeleton behavior as a model for switching grid-cols depending on
ownership; keep BookingSection implementation unchanged and ensure page.tsx
imports and uses the new client wrapper instead of the static md:grid-cols-3
div.

Comment on lines +39 to 67
let bannerImageUrl = '';
if (typeof mainImage === 'string') {
bannerImageUrl = mainImage;
} else if (mainImage instanceof File) {
bannerImageUrl = await uploadImage(mainImage);
}

const subImageUrls: string[] = [];
for (const img of subImage) {
if (img instanceof File) {
const url = await uploadImage(img);
subImageUrls.push(url);
} else if (typeof img === 'string') {
subImageUrls.push(img);
}
}

const payload = {
title,
category,
description,
address,
price: parsedPrice,
schedules: dates,
bannerImageUrl: mainImage,
subImageUrls: subImage,
bannerImageUrl,
subImageUrls,
};

const res = await privateInstance.post('/addActivity', payload);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

이미지 업로드 후 체험 생성 API 실패 시 고아(orphaned) 리소스 발생 가능

bannerImageUrl·subImageUrls 업로드가 완료된 뒤 /addActivity POST가 실패하면 이미 스토리지에 올라간 이미지 파일들이 고아 상태로 남습니다. 등록 수가 많아질수록 불필요한 스토리지 비용이 누적됩니다. 현재 아키텍처의 구조적 한계이지만, 향후 다음 중 하나를 고려할 수 있습니다.

  • 서버 측 처리: 이미지 업로드와 체험 생성을 하나의 트랜잭션 또는 서버 액션으로 묶어 원자성 보장
  • 주기적 정리: 미사용 이미지를 정리하는 배치 작업 또는 TTL 정책 추가
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/myactivity/hooks/useCreateActivityForm.ts around lines
39 - 67, The current flow uploads bannerImageUrl and subImageUrls via
uploadImage before calling privateInstance.post('/addActivity'), which can leave
orphaned uploaded files if the POST fails; wrap the upload + POST sequence in a
try/catch and on any failure delete any already-uploaded images: track uploaded
URLs in bannerImageUrl and subImageUrls, and in the catch call your storage
deletion helper (e.g., deleteImage or an API endpoint you add) for each uploaded
URL, then rethrow or surface the error; update useCreateActivityForm to perform
uploads, attempt privateInstance.post('/addActivity', payload) inside the same
try, and cleanup uploaded images on catch to avoid orphaned resources.

Comment on lines +47 to +54
for (const img of subImage) {
if (img instanceof File) {
const url = await uploadImage(img);
subImageUrls.push(url);
} else if (typeof img === 'string') {
subImageUrls.push(img);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

서브 이미지 업로드를 병렬로 처리하세요.

for...of + await 패턴은 서브 이미지를 순차적으로 업로드하여 최대 4개 파일을 직렬 처리합니다. Promise.all로 병렬 처리하면 전체 업로드 시간을 단축할 수 있습니다.

⚡ 병렬 업로드로 리팩토링 제안
-      const subImageUrls: string[] = [];
-      for (const img of subImage) {
-        if (img instanceof File) {
-          const url = await uploadImage(img);
-          subImageUrls.push(url);
-        } else if (typeof img === 'string') {
-          subImageUrls.push(img);
-        }
-      }
+      const subImageUrls = await Promise.all(
+        subImage.map((img) =>
+          img instanceof File ? uploadImage(img) : Promise.resolve(img as string),
+        ),
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const img of subImage) {
if (img instanceof File) {
const url = await uploadImage(img);
subImageUrls.push(url);
} else if (typeof img === 'string') {
subImageUrls.push(img);
}
}
const subImageUrls = await Promise.all(
subImage.map((img) =>
img instanceof File ? uploadImage(img) : Promise.resolve(img as string),
),
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/myactivity/hooks/useCreateActivityForm.ts around lines
47 - 54, Replace the sequential for...of+await loop that uploads each entry in
subImage with a parallel Promise.all approach: map subImage to an array of
promises where each element returns uploadImage(img) if img is a File or returns
the img string otherwise, then await Promise.all on that array and assign the
resulting array to subImageUrls (or push the results) so uploads happen
concurrently; locate the logic in useCreateActivityForm where subImage,
uploadImage, and subImageUrls are referenced.

},
);

revalidateTag(`activity-${id}`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Next.js revalidateTag API documentation single argument deprecated

💡 Result:

revalidateTag(tag) with a single argument is deprecated in recent Next.js versions. The docs now require a second argument that specifies the cache revalidation behavior (a cache-life “profile”). [1]

What to use instead

Recommended (stale-while-revalidate):

import { revalidateTag } from 'next/cache'

revalidateTag('posts', 'max')

This marks the tag as stale and refreshes in the background on the next visit (SWR semantics). [1][2]

If you need immediate expiration (read-your-own-writes):

  • In Server Actions, use:
import { updateTag } from 'next/cache'

updateTag('posts')

updateTag is Server Actions only and causes the next request to be a blocking revalidate/cache miss. [1][3]

  • In Route Handlers (where updateTag is not allowed), you can pass an explicit expire behavior via the second argument (e.g. { expire: 0 }) per the updated API reference. [2]

Relevant docs

  • Next.js error doc: “revalidateTag Single Argument Deprecated” [1]
  • API reference: revalidateTag(tag: string, profile: string | { expire?: number }) [2]
  • Docs mismatch discussion/context (profile shown optional in some places): GitHub issue #85281 [4]

Sources: [1] [2] [3] [4]


🏁 Script executed:

cd src/app/api/editActivity && find . -name "route.ts" -type f && head -50 "[id]/route.ts"

Repository: codeit-2team/GlobalNomad

Length of output: 1531


🏁 Script executed:

rg "revalidateTag" --type ts --type tsx -n

Repository: codeit-2team/GlobalNomad

Length of output: 95


🏁 Script executed:

rg "revalidateTag" --type ts -n

Repository: codeit-2team/GlobalNomad

Length of output: 603


revalidateTag 단일 인자 형식은 Deprecated — 두 번째 인자를 추가하세요.

공식 Next.js 문서에 따르면, revalidateTag(tag) 단일 인자 형식은 deprecated이며 TypeScript 오류가 발생합니다. 이 Route Handler에서는 권장되는 형식인 revalidateTag(tag, 'max')를 사용하세요. 'max'는 stale-while-revalidate(SWR) 의미론으로 작동하며, 다음 요청 시 백그라운드에서 갱신됩니다.

🛠️ 수정 제안
-    revalidateTag(`activity-${id}`);
+    revalidateTag(`activity-${id}`, 'max');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/editActivity/`[id]/route.ts at line 40, The call to revalidateTag
in the Route Handler uses the deprecated single-argument form; update the
invocation of revalidateTag(`activity-${id}`) to the recommended two-argument
form revalidateTag(`activity-${id}`, 'max') so TypeScript errors are resolved
and the tag is revalidated using SWR semantics; locate the usage of
revalidateTag in route.ts and change the call accordingly.

Comment on lines +42 to +44
if (response.data?.activityId) {
revalidateTag(`activity-${response.data.activityId}`);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for review creation API response type definitions
echo "=== Looking for review-related types and API responses ==="
rg -n --type=ts "response.*activityId|activityId.*response" -B3 -A3

Repository: codeit-2team/GlobalNomad

Length of output: 712


🏁 Script executed:

#!/bin/bash
# Search for revalidateTag usage to understand the API signature
echo "=== Checking revalidateTag usage patterns ==="
rg -n "revalidateTag" --type=ts -B2 -A2

Repository: codeit-2team/GlobalNomad

Length of output: 2434


🏁 Script executed:

#!/bin/bash
# Look at the specific file mentioned in the review
echo "=== Examining the review route file ==="
cat -n src/app/api/reservations/[id]/reviews/route.ts

Repository: codeit-2team/GlobalNomad

Length of output: 2077


🏁 Script executed:

#!/bin/bash
# Search for review API response type definitions
echo "=== Looking for review response types ==="
rg -n "interface.*Review|type.*Review" --type=ts | head -30

Repository: codeit-2team/GlobalNomad

Length of output: 629


🏁 Script executed:

#!/bin/bash
# Search for the mypage/reservations/page.tsx file to understand the client pattern
echo "=== Checking client-side reservation/review code ==="
fd "reservations.*page" --type=ts --type=tsx

Repository: codeit-2team/GlobalNomad

Length of output: 307


🏁 Script executed:

#!/bin/bash
# Check the ReviewResponse and CreateReviewRequest interfaces
echo "=== Examining ReviewResponse and CreateReviewRequest ==="
cat -n src/types/reservationTypes.ts | grep -A20 "interface ReviewResponse\|interface CreateReviewRequest"

Repository: codeit-2team/GlobalNomad

Length of output: 549


🏁 Script executed:

#!/bin/bash
# Find mypage/reservations files
echo "=== Finding mypage/reservations files ==="
find . -path "*/mypage/reservations*" -type f | head -20

Repository: codeit-2team/GlobalNomad

Length of output: 544


🏁 Script executed:

#!/bin/bash
# Search for review-related code in mypage directory
echo "=== Looking for review code in mypage ==="
rg -n "activityId" --type=ts --type=tsx src/app/\*mypage\*

Repository: codeit-2team/GlobalNomad

Length of output: 154


🏁 Script executed:

#!/bin/bash
# Check Next.js version to understand revalidateTag API
echo "=== Checking package.json for Next.js version ==="
grep -A2 '"next"' package.json

Repository: codeit-2team/GlobalNomad

Length of output: 192


🏁 Script executed:

#!/bin/bash
# Check mypage/reservations/page.tsx to understand the client pattern
echo "=== Examining mypage/reservations/page.tsx ==="
cat -n src/app/\(with-header\)/mypage/reservations/page.tsx

Repository: codeit-2team/GlobalNomad

Length of output: 11281


🏁 Script executed:

#!/bin/bash
# Check ReviewModal.tsx to see how reviews are created
echo "=== Examining ReviewModal.tsx ==="
cat -n src/app/\(with-header\)/mypage/reservations/components/ReviewModal.tsx | head -100

Repository: codeit-2team/GlobalNomad

Length of output: 3934


🏁 Script executed:

#!/bin/bash
# Search for revalidateTag usage patterns in Next.js 15.x context
echo "=== Checking if revalidateTag is used with second argument anywhere ==="
rg "revalidateTag.*\)" --type ts -A1 -B1

Repository: codeit-2team/GlobalNomad

Length of output: 830


조건부 guard를 제거하고 revalidateTag 호출을 항상 실행하세요.

ReviewResponse 타입 정의에서 activityId는 필수 필드이므로 (activityId: number;), 요청이 성공하면 응답에 activityId가 항상 포함됩니다. if (response.data?.activityId) 조건은 불필요한 방어적 코딩입니다. 또한 revalidateTag는 현재 단일 인자 형식이 표준이며, 이미 전체 코드베이스에서 일관되게 사용되고 있습니다.

🛠️ 수정 예시
-    if (response.data?.activityId) {
-      revalidateTag(`activity-${response.data.activityId}`);
-    }
+    revalidateTag(`activity-${response.data.activityId}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/reservations/`[id]/reviews/route.ts around lines 42 - 44, Remove
the unnecessary conditional guard and always call revalidateTag using the
required activityId from the ReviewResponse; specifically, in the route handler
replace the if-check around revalidateTag and directly invoke
revalidateTag(`activity-${response.data.activityId}`), relying on the
ReviewResponse.type (activityId: number) and the existing single-argument
revalidateTag usage.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(with-header)/activities/[id]/components/BookingSection.tsx:
- Line 60: The enabled condition includes redundant checks (!!year && !!month)
that are always truthy since year and month are initialized, so remove them and
change the enabled expression to only depend on activityId and isOwner (e.g.,
use activityId presence and !isOwner) in the same object where enabled is
defined (referencing enabled, activityId, year, month, isOwner to locate the
line).
- Line 27: The condition userId != null is redundant because the userId prop is
typed as number; update the isOwner calculation by removing that redundant guard
so it reads a single check comparing currentUserId and userId while still
guarding currentUserId (e.g., replace the expression in the isOwner constant
with a null-check only for currentUserId and direct equality to userId); target
the isOwner declaration in BookingSection.tsx and the userId prop usage to
implement this simplification.
- Around line 32-58: The code currently awaits currentResponse before starting
prev/next requests causing serial latency; change to start all three requests
concurrently by creating promises with privateInstance.get for the current month
and the two side months (using activityId and padMonth) before awaiting; then
await them together (e.g., via Promise.allSettled), ensure you check the current
month's promise result first and if it rejected rethrow the error to preserve
previous failure behavior, and finally collect fulfilled side responses
(filtering PromiseFulfilledResult for the two side promises) and merge their
data with the currentResponse.data.

---

Duplicate comments:
In `@src/app/`(with-header)/activities/[id]/components/BookingSection.tsx:
- Around line 63-68: The handler handleMonthChange currently wraps setYear and
setMonth in an unnecessary setTimeout; remove the setTimeout wrapper so the
function directly calls setYear(year) and setMonth(month). Keep it as a
useCallback and ensure the callback references the existing state setters
(setYear, setMonth) so the dependency array remains appropriate (they are stable
from useState).

const currentUserId = useUserStore((state) =>
state.user ? state.user.id : null,
);
const isOwner = currentUserId != null && userId != null && currentUserId === userId;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

userId != null 조건이 항상 true이므로 제거 가능합니다.

userId prop이 number로 선언되어 있으므로 userId != null 가드는 불필요합니다.

♻️ 수정 제안
-const isOwner = currentUserId != null && userId != null && currentUserId === userId;
+const isOwner = currentUserId != null && currentUserId === userId;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isOwner = currentUserId != null && userId != null && currentUserId === userId;
const isOwner = currentUserId != null && currentUserId === userId;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/activities/[id]/components/BookingSection.tsx at line
27, The condition userId != null is redundant because the userId prop is typed
as number; update the isOwner calculation by removing that redundant guard so it
reads a single check comparing currentUserId and userId while still guarding
currentUserId (e.g., replace the expression in the isOwner constant with a
null-check only for currentUserId and direct equality to userId); target the
isOwner declaration in BookingSection.tsx and the userId prop usage to implement
this simplification.

Comment on lines +32 to +58
const prevMonth = month === 1 ? 12 : month - 1;
const prevYear = month === 1 ? year - 1 : year;
const nextMonth = month === 12 ? 1 : month + 1;
const nextYear = month === 12 ? year + 1 : year;


const currentResponse = await privateInstance.get<GroupedSchedule[]>(
`/activities/${activityId}/available-schedule?year=${year}&month=${padMonth(month)}`,
);


const sideResults = await Promise.allSettled([
privateInstance.get<GroupedSchedule[]>(
`/activities/${activityId}/available-schedule?year=${prevYear}&month=${padMonth(prevMonth)}`,
),
privateInstance.get<GroupedSchedule[]>(
`/activities/${activityId}/available-schedule?year=${nextYear}&month=${padMonth(nextMonth)}`,
),
]);

const sideData = sideResults
.filter(
(r): r is PromiseFulfilledResult<AxiosResponse<GroupedSchedule[]>> => r.status === 'fulfilled',
)
.flatMap((r) => r.value.data);

return [...sideData, ...currentResponse.data];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

현재 월 요청이 이전/다음 월 요청을 블로킹하여 직렬 레이턴시가 발생합니다.

현재 흐름에서 currentResponseawait한 뒤에야 sideResults의 두 요청이 시작됩니다. 세 요청을 Promise.allSettled로 동시에 실행하면 총 소요 시간이 T(현재) + max(T(이전), T(다음))에서 max(T(현재), T(이전), T(다음))으로 단축됩니다. 현재 월 요청 실패 시 전체 쿼리를 실패시키는 기존 동작도 그대로 유지할 수 있습니다.

⚡ 수정 제안: 세 요청 병렬화
-      const currentResponse = await privateInstance.get<GroupedSchedule[]>(
-        `/activities/${activityId}/available-schedule?year=${year}&month=${padMonth(month)}`,
-      );
-
-      const sideResults = await Promise.allSettled([
-        privateInstance.get<GroupedSchedule[]>(
-          `/activities/${activityId}/available-schedule?year=${prevYear}&month=${padMonth(prevMonth)}`,
-        ),
-        privateInstance.get<GroupedSchedule[]>(
-          `/activities/${activityId}/available-schedule?year=${nextYear}&month=${padMonth(nextMonth)}`,
-        ),
-      ]);
+      const [currentResult, ...sideResults] = await Promise.allSettled([
+        privateInstance.get<GroupedSchedule[]>(
+          `/activities/${activityId}/available-schedule?year=${year}&month=${padMonth(month)}`,
+        ),
+        privateInstance.get<GroupedSchedule[]>(
+          `/activities/${activityId}/available-schedule?year=${prevYear}&month=${padMonth(prevMonth)}`,
+        ),
+        privateInstance.get<GroupedSchedule[]>(
+          `/activities/${activityId}/available-schedule?year=${nextYear}&month=${padMonth(nextMonth)}`,
+        ),
+      ]);
+
+      if (currentResult.status === 'rejected') throw currentResult.reason;

       const sideData = sideResults
         .filter(
           (r): r is PromiseFulfilledResult<AxiosResponse<GroupedSchedule[]>> => r.status === 'fulfilled',
         )
         .flatMap((r) => r.value.data);

-      return [...sideData, ...currentResponse.data];
+      return [...sideData, ...currentResult.value.data];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/activities/[id]/components/BookingSection.tsx around
lines 32 - 58, The code currently awaits currentResponse before starting
prev/next requests causing serial latency; change to start all three requests
concurrently by creating promises with privateInstance.get for the current month
and the two side months (using activityId and padMonth) before awaiting; then
await them together (e.g., via Promise.allSettled), ensure you check the current
month's promise result first and if it rejected rethrow the error to preserve
previous failure behavior, and finally collect fulfilled side responses
(filtering PromiseFulfilledResult for the two side promises) and merge their
data with the currentResponse.data.


return [...sideData, ...currentResponse.data];
},
enabled: !!activityId && !!year && !!month && !isOwner,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

!!year && !!month 조건이 항상 true이므로 제거 가능합니다.

yearnew Date().getFullYear()(예: 2026), month1~12 범위의 값으로 초기화되어 둘 다 항상 truthy입니다.

♻️ 수정 제안
-    enabled: !!activityId && !!year && !!month && !isOwner,
+    enabled: !!activityId && !isOwner,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
enabled: !!activityId && !!year && !!month && !isOwner,
enabled: !!activityId && !isOwner,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(with-header)/activities/[id]/components/BookingSection.tsx at line
60, The enabled condition includes redundant checks (!!year && !!month) that are
always truthy since year and month are initialized, so remove them and change
the enabled expression to only depend on activityId and isOwner (e.g., use
activityId presence and !isOwner) in the same object where enabled is defined
(referencing enabled, activityId, year, month, isOwner to locate the line).

@evaain706 evaain706 merged commit 6032d36 into develop Feb 21, 2026
2 checks passed
@evaain706 evaain706 deleted the fix/160 branch February 21, 2026 15:21
@evaain706 evaain706 changed the title Fix/160 Fix/160 리팩토링 Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant